
'''

wxPython Custom Widget Collection 20060207
Written By: Edward Flick (eddy -=at=- cdf-imaging -=dot=- com)
            Michele Petrazzo (michele -=dot=- petrazzo -=at=- unipex -=dot=- it)
            Will Sadkin (wsadkin-=at=- nameconnector -=dot=- com)
Copyright 2006 (c) CDF Inc. ( http://www.cdf-imaging.com )
Contributed to the wxPython project under the wxPython project's license.

Modified by D. Guez to allow to choose only the text between commas

'''

import locale, wx, sys, cStringIO

import  wx.lib.mixins.listctrl  as  listmix

from wx import ImageFromStream, BitmapFromImage
#----------------------------------------------------------------------
def getSmallUpArrowData():
    return \
'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x10\x00\x00\x00\x10\x08\x06\
\x00\x00\x00\x1f\xf3\xffa\x00\x00\x00\x04sBIT\x08\x08\x08\x08|\x08d\x88\x00\
\x00\x00<IDAT8\x8dcddbf\xa0\x040Q\xa4{h\x18\xf0\xff\xdf\xdf\xffd\x1b\x00\xd3\
\x8c\xcf\x10\x9c\x06\xa0k\xc2e\x08m\xc2\x00\x97m\xd8\xc41\x0c \x14h\xe8\xf2\
\x8c\xa3)q\x10\x18\x00\x00R\xd8#\xec\xb2\xcd\xc1Y\x00\x00\x00\x00IEND\xaeB`\
\x82'

def getSmallUpArrowBitmap():
    return BitmapFromImage(getSmallUpArrowImage())

def getSmallUpArrowImage():
    stream = cStringIO.StringIO(getSmallUpArrowData())
    return ImageFromStream(stream)


def getSmallDnArrowData():
    return \
"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x10\x00\x00\x00\x10\x08\x06\
\x00\x00\x00\x1f\xf3\xffa\x00\x00\x00\x04sBIT\x08\x08\x08\x08|\x08d\x88\x00\
\x00\x00HIDAT8\x8dcddbf\xa0\x040Q\xa4{\xd4\x00\x06\x06\x06\x06\x06\x16t\x81\
\xff\xff\xfe\xfe'\xa4\x89\x91\x89\x99\x11\xa7\x0b\x90%\ti\xc6j\x00>C\xb0\x89\
\xd3.\x10\xd1m\xc3\xe5*\xbc.\x80i\xc2\x17.\x8c\xa3y\x81\x01\x00\xa1\x0e\x04e\
?\x84B\xef\x00\x00\x00\x00IEND\xaeB`\x82"

def getSmallDnArrowBitmap():
    return BitmapFromImage(getSmallDnArrowImage())

def getSmallDnArrowImage():
    stream = cStringIO.StringIO(getSmallDnArrowData())
    return ImageFromStream(stream)
#----------------------------------------------------------------------

class myListCtrl(wx.ListCtrl, listmix.ListCtrlAutoWidthMixin):
    def __init__(self, parent, ID=-1, pos=wx.DefaultPosition,
                 size=wx.DefaultSize, style=0):
        wx.ListCtrl.__init__(self, parent, ID, pos, size, style)
        listmix.ListCtrlAutoWidthMixin.__init__(self)

class TextCtrlAutoComplete (wx.TextCtrl, listmix.ColumnSorterMixin ):

    def __init__ ( self, parent, colNames=None, choices = None,
                  multiChoices=None, showHead=True, dropDownClick=True,
                  colFetch=-1, colSearch=0, hideOnNoMatch=True,
                  selectCallback=None, entryCallback=None, matchFunction=None,
                  dynamicChoiceCallback=None,getWorkingString=None,applyItem=None,
                  **therest) :
        '''
        Constructor works just like wx.TextCtrl except you can pass in a
        list of choices.  You can also change the choice list at any time
        by calling setChoices.
        '''

        if therest.has_key('style'):
            therest['style']=wx.TE_PROCESS_ENTER | therest['style']
        else:
            therest['style']=wx.TE_PROCESS_ENTER

        wx.TextCtrl.__init__(self, parent, **therest )

        #Some variables
        self._dropDownClick = dropDownClick
        self._colNames = colNames
        self._multiChoices = multiChoices
        self._showHead = showHead
        self._choices = choices
        self._lastinsertionpoint = 0
        self._hideOnNoMatch = hideOnNoMatch
        self._selectCallback = selectCallback
        self._entryCallback = entryCallback
        self._matchFunction = matchFunction
        self._dynamicChoiceCallback = dynamicChoiceCallback
        self._getWorkingString = getWorkingString
        self._applyItem = applyItem
        self._mustHide = False

        self._screenheight = wx.SystemSettings.GetMetric( wx.SYS_SCREEN_Y )

        #sort variable needed by listmix
        self.itemDataMap = dict()

        #Load and sort data
        if not (self._multiChoices or self._choices):
            raise ValueError, "Pass me at least one of multiChoices OR choices"

        #widgets
        self.dropdown = wx.PopupWindow( self )

        #Control the style
        flags = wx.LC_REPORT | wx.LC_SINGLE_SEL | wx.LC_SORT_ASCENDING
        if not (showHead and multiChoices) :
            flags = flags | wx.LC_NO_HEADER

        #Create the list and bind the events
        self.dropdownlistbox = myListCtrl( self.dropdown, style=flags,
                                 pos=wx.Point( 0, 0) )

        #initialize the parent
        if multiChoices: ln = len(multiChoices)
        else: ln = 1
        #else: ln = len(choices)
        listmix.ColumnSorterMixin.__init__(self, ln)

        #load the data
        if multiChoices: self.SetMultipleChoices (multiChoices, colSearch=colSearch, colFetch=colFetch)
        else: self.SetChoices ( choices )

        gp = self
        while ( gp != None ) :
            gp.Bind ( wx.EVT_MOVE , self.onControlChanged, gp )
            gp.Bind ( wx.EVT_SIZE , self.onControlChanged, gp )
            gp = gp.GetParent()

        self.Bind( wx.EVT_KILL_FOCUS, self.onControlChanged, self )
        self.Bind( wx.EVT_TEXT , self.onEnteredText, self )
        self.Bind( wx.EVT_KEY_DOWN , self.onKeyDown, self )

        #If need drop down on left click
        if dropDownClick:
            self.Bind ( wx.EVT_LEFT_DOWN , self.onClickToggleDown, self )
            self.Bind ( wx.EVT_LEFT_UP , self.onClickToggleUp, self )

        self.dropdown.Bind( wx.EVT_LISTBOX , self.onListItemSelected, self.dropdownlistbox )
        self.dropdownlistbox.Bind(wx.EVT_LEFT_DOWN, self.onListClick)
        self.dropdownlistbox.Bind(wx.EVT_LEFT_DCLICK, self.onListDClick)
        self.dropdownlistbox.Bind(wx.EVT_LIST_COL_CLICK, self.onListColClick)

        self.il = wx.ImageList(16, 16)

        self.sm_dn = self.il.Add(getSmallDnArrowBitmap())
        self.sm_up = self.il.Add(getSmallUpArrowBitmap())

        self.dropdownlistbox.SetImageList(self.il, wx.IMAGE_LIST_SMALL)
        self._ascending = True


    #-- methods called from mixin class
    def GetSortImages(self):
        return (self.sm_dn, self.sm_up)

    def GetListCtrl(self):
        return self.dropdownlistbox

    # -- event methods
    def onListClick(self, evt):
        toSel, flag = self.dropdownlistbox.HitTest( evt.GetPosition() )
        #no values on poition, return
        if toSel == -1: return
        self.dropdownlistbox.Select(toSel)

    def onListDClick(self, evt):
        self._setValueFromSelected()

    def onListColClick(self, evt):
        col = evt.GetColumn()

        #reverse the sort
        if col == self._colSearch:
            self._ascending = not self._ascending

        self.SortListItems( evt.GetColumn(), ascending=self._ascending )
        self._colSearch = evt.GetColumn()
        evt.Skip()

    def onEnteredText(self, event):
        if self._getWorkingString:
            text = self._getWorkingString()
        else:
            text = event.GetString()
        if self._entryCallback:
            self._entryCallback()

        if not text:
            # control is empty; hide dropdown if shown:
            if self.dropdown.IsShown():
                self._showDropDown(False)
            event.Skip()
            return

        found = False
        if self._dynamicChoiceCallback:
            choices = self._dynamicChoiceCallback(text)
        if self._multiChoices:
            #load the sorted data into the listbox
            dd = self.dropdownlistbox
            choices = [dd.GetItem(x, self._colSearch).GetText()
                for x in xrange(dd.GetItemCount())]
        else:
            choices = self._choices

        for numCh, choice in enumerate(choices):
            try:
                if self._matchFunction and self._matchFunction(text, choice):
                    found = True
                elif choice.lower().startswith(text.lower()) :
                    found = True
            except:
                found=False
            if found:
                self._showDropDown(True)
                item = self.dropdownlistbox.GetItem(numCh)
                toSel = item.GetId()
                self.dropdownlistbox.Select(toSel)
                break

        if not found:
            self.dropdownlistbox.Select(self.dropdownlistbox.GetFirstSelected(), False)
            if self._hideOnNoMatch:
                self._showDropDown(False)

        self._listItemVisible()

        event.Skip ()

    def onKeyDown ( self, event ) :
        """ Do some work when the user press on the keys:
            up and down: move the cursor
            left and right: move the search
        """
        skip = True
        sel = self.dropdownlistbox.GetFirstSelected()
        visible = self.dropdown.IsShown()

        KC = event.GetKeyCode()
        if KC == wx.WXK_DOWN :
            if sel < (self.dropdownlistbox.GetItemCount () - 1) :
                self.dropdownlistbox.Select ( sel+1 )
                self._listItemVisible()
            self._showDropDown ()
            skip = False
        elif KC == wx.WXK_UP :
            if sel > 0 :
                self.dropdownlistbox.Select ( sel - 1 )
                self._listItemVisible()
            self._showDropDown ()
            skip = False
        elif KC == wx.WXK_LEFT :
            if not self._multiChoices:
                event.Skip()
                return
            if self._colSearch > 0:
                self._colSearch -=1
            self._showDropDown ()
        elif KC == wx.WXK_RIGHT:
            if not self._multiChoices:
                event.Skip()
                return
            if self._colSearch < self.dropdownlistbox.GetColumnCount() -1:
                self._colSearch += 1
            self._showDropDown()

        if visible :
            if event.GetKeyCode() == wx.WXK_RETURN :
                self._setValueFromSelected()
                skip = False
            if event.GetKeyCode() == wx.WXK_ESCAPE :
                self._showDropDown( False )
                skip = False
        if skip :
            event.Skip()

    def onListItemSelected (self, event):
        self._setValueFromSelected()
        event.Skip()

    def onClickToggleDown(self, event):
        self._lastinsertionpoint = self.GetInsertionPoint()
        event.Skip ()

    def onClickToggleUp ( self, event ) :
        if ( self.GetInsertionPoint() == self._lastinsertionpoint ) :
            self._showDropDown ( not self.dropdown.IsShown() )
        event.Skip ()

    def onControlChanged(self, event):
        try:
            self._showDropDown( False )
            event.Skip()
        except:
            pass


    # -- Interfaces methods
    def SetMultipleChoices(self, choices, colSearch=0, colFetch=-1):
        ''' Set multi-column choice
        '''
        self._multiChoices = choices
        self._choices = None
        if not isinstance(self._multiChoices, list):
            self._multiChoices = [ x for x in self._multiChoices]

        flags = wx.LC_REPORT | wx.LC_SINGLE_SEL | wx.LC_SORT_ASCENDING
        if not self._showHead:
            flags |= wx.LC_NO_HEADER
        self.dropdownlistbox.SetWindowStyleFlag(flags)

        #prevent errors on "old" systems
        if sys.version.startswith("2.3"):
            self._multiChoices.sort(lambda x, y: cmp(x[0].lower(), y[0].lower()))
        else:
            self._multiChoices.sort(key=lambda x: locale.strxfrm(x[0]).lower() )

        self._updateDataList(self._multiChoices)

        lChoices = len(choices)
        if lChoices < 2:
            raise ValueError, "You have to pass me a multi-dimension list"

        for numCol, rowValues in enumerate(choices[0]):

            if self._colNames: colName = self._colNames[numCol]
            else: colName = "Select %i" % numCol

            self.dropdownlistbox.InsertColumn(numCol, colName)

        for numRow, valRow in enumerate(choices):

            for numCol, colVal in enumerate(valRow):
                if numCol == 0:
                    index = self.dropdownlistbox.InsertImageStringItem(sys.maxint, colVal, -1)
                self.dropdownlistbox.SetStringItem(index, numCol, colVal)
                self.dropdownlistbox.SetItemData(index, numRow)

        self._setListSize()
        self._colSearch = colSearch
        self._colFetch = colFetch

    def SetValue(self,v):
        s = self._mustHide
        self._mustHide=True
        super(TextCtrlAutoComplete , self).SetValue(v)
        self._mustHide = s

    def SetChoices(self, choices):
        '''
        Sets the choices available in the popup wx.ListBox.
        The items will be sorted case insensitively.
        '''
        self._choices = choices
        self._multiChoices = None
        flags = wx.LC_REPORT | wx.LC_SINGLE_SEL | wx.LC_SORT_ASCENDING | wx.LC_NO_HEADER
        self.dropdownlistbox.SetWindowStyleFlag(flags)

        if not isinstance(choices, list):
            self._choices = [ x for x in choices]

        #prevent errors on "old" systems
        if sys.version.startswith("2.3"):
            self._choices.sort(lambda x, y: cmp(x.lower(), y.lower()))
        else:
            self._choices.sort(key=lambda x: locale.strxfrm(x).lower())

        self._updateDataList(self._choices)

        self.dropdownlistbox.InsertColumn(0, "")

        for num, colVal in enumerate(self._choices):
            index = self.dropdownlistbox.InsertImageStringItem(sys.maxint, colVal, -1)

            self.dropdownlistbox.SetStringItem(index, 0, colVal)
            self.dropdownlistbox.SetItemData(index, num)

        self._setListSize()

        # there is only one choice for both search and fetch if setting a single column:
        self._colSearch = 0
        self._colFetch = -1
        if len(choices) == 0 : self._showDropDown(False)

    def GetChoices(self):
        if self._choices:
            return self._choices
        else:
            return self._multiChoices

    def SetSelectCallback(self, cb=None):
        self._selectCallback = cb

    def SetEntryCallback(self, cb=None):
        self._entryCallback = cb

    def SetMatchFunction(self, mf=None):
        self._matchFunction = mf


    #-- Internal methods
    def _setValueFromSelected( self ) :
        '''
        Sets the wx.TextCtrl value from the selected wx.ListCtrl item.
        Will do nothing if no item is selected in the wx.ListCtrl.
        '''
        sel = self.dropdownlistbox.GetFirstSelected()
        if sel > -1:
            if self._colFetch != -1: col = self._colFetch
            else: col = self._colSearch

            itemtext = self.dropdownlistbox.GetItem(sel, col).GetText()
            if self._selectCallback:
                dd = self.dropdownlistbox
                values = [dd.GetItem(sel, x).GetText()
                    for x in xrange(dd.GetColumnCount())]
                self._selectCallback( values )

            if self._applyItem:
                self._applyItem(itemtext)
            else:
                self.SetValue (itemtext)
                self.SetInsertionPointEnd ()
            #self.SetSelection ( -1, -1 )
            self._showDropDown ( False )


    def _showDropDown ( self, show = True ) :
        '''
        Either display the drop down list (show = True) or hide it (show = False).
        '''
        mustHide = self._mustHide or len(self._choices)==0
        if not mustHide and show :
            size = self.dropdown.GetSize()
            width, height = self . GetSizeTuple()
            x, y = self . ClientToScreenXY ( 0, height )
            if size.GetWidth() != width :
                size.SetWidth(width)
                self.dropdown.SetSize(size)
                self.dropdownlistbox.SetSize(self.dropdown.GetClientSize())
            if (y + size.GetHeight()) < self._screenheight :
                self.dropdown . SetPosition ( wx.Point(x, y) )
            else:
                self.dropdown . SetPosition ( wx.Point(x, y - height - size.GetHeight()) )
        if not show or not mustHide : self.dropdown.Show ( show )

    def _listItemVisible( self ) :
        '''
        Moves the selected item to the top of the list ensuring it is always visible.
        '''
        toSel =  self.dropdownlistbox.GetFirstSelected ()
        if toSel == -1: return
        self.dropdownlistbox.EnsureVisible( toSel )

    def _updateDataList(self, choices):
        #delete, if need, all the previous data
        if self.dropdownlistbox.GetColumnCount() != 0:
            self.dropdownlistbox.DeleteAllColumns()
            self.dropdownlistbox.DeleteAllItems()

        #and update the dict
        if choices:
            for numVal, data in enumerate(choices):
                self.itemDataMap[numVal] = data
        else:
            numVal = 0
        self.SetColumnCount(numVal)

    def _setListSize(self):
        if self._multiChoices:
            choices = self._multiChoices
        else:
            choices = self._choices

        longest = 0
        for choice in choices :
            longest = max(len(choice), longest)

        longest += 3
        itemcount = min( len( choices ) , 7 ) + 2
        charheight = self.dropdownlistbox.GetCharHeight()
        charwidth = self.dropdownlistbox.GetCharWidth()
        self.popupsize = wx.Size( charwidth*longest, charheight*itemcount )
        self.dropdownlistbox.SetSize ( self.popupsize )
        self.dropdown.SetClientSize( self.popupsize )




class test:
    def __init__(self):
        args = dict()
        if 1:
            args["colNames"] = ("col1", "col2")
            args["multiChoices"] = [ ("Zoey","WOW"), ("Alpha", "wxPython"),
                                    ("Ceda","Is"), ("Beta", "fantastic"),
                                    ("zoebob", "!!")]
            args["colFetch"] = 1
        else:
            args["choices"] = ["123", "cs", "cds", "Bob","Marley","Alpha"]

        args["selectCallback"] = self.selectCallback

        self.dynamic_choices = [
                        'aardvark', 'abandon', 'acorn', 'acute', 'adore',
                        'aegis', 'ascertain', 'asteroid',
                        'beautiful', 'bold', 'classic',
                        'daring', 'dazzling', 'debonair', 'definitive',
                        'effective', 'elegant',
                        'http://python.org', 'http://www.google.com',
                        'fabulous', 'fantastic', 'friendly', 'forgiving', 'feature',
                        'sage', 'scarlet', 'scenic', 'seaside', 'showpiece', 'spiffy',
                        'www.wxPython.org', 'www.osafoundation.org'
                        ]


        app = wx.PySimpleApp()
        frm = wx.Frame(None,-1,"Test",style=wx.TAB_TRAVERSAL|wx.DEFAULT_FRAME_STYLE)
        panel = wx.Panel(frm)
        sizer = wx.BoxSizer(wx.VERTICAL)

        self._ctrl = TextCtrlAutoComplete(panel, **args)
        but = wx.Button(panel,label="Set other multi-choice")
        but.Bind(wx.EVT_BUTTON, self.onBtMultiChoice)
        but2 = wx.Button(panel,label="Set other one-colum choice")
        but2.Bind(wx.EVT_BUTTON, self.onBtChangeChoice)
        but3 = wx.Button(panel,label="Set the starting choices")
        but3.Bind(wx.EVT_BUTTON, self.onBtStartChoices)
        but4 = wx.Button(panel,label="Enable dynamic choices")
        but4.Bind(wx.EVT_BUTTON, self.onBtDynamicChoices)

        sizer.Add(but, 0, wx.ADJUST_MINSIZE, 0)
        sizer.Add(but2, 0, wx.ADJUST_MINSIZE, 0)
        sizer.Add(but3, 0, wx.ADJUST_MINSIZE, 0)
        sizer.Add(but4, 0, wx.ADJUST_MINSIZE, 0)
        sizer.Add(self._ctrl, 0, wx.EXPAND|wx.ADJUST_MINSIZE, 0)
        panel.SetAutoLayout(True)
        panel.SetSizer(sizer)
        sizer.Fit(panel)
        sizer.SetSizeHints(panel)
        panel.Layout()
        app.SetTopWindow(frm)
        frm.Show()
        but.SetFocus()
        app.MainLoop()

    def onBtChangeChoice(self, event):
        #change the choices
        self._ctrl.SetChoices(["123", "cs", "cds", "Bob","Marley","Alpha"])
        self._ctrl.SetEntryCallback(None)
        self._ctrl.SetMatchFunction(None)

    def onBtMultiChoice(self, event):
        #change the choices
        self._ctrl.SetMultipleChoices( [ ("Test","Hello"), ("Other word","World"),
                                        ("Yes!","it work?") ], colFetch = 1 )
        self._ctrl.SetEntryCallback(None)
        self._ctrl.SetMatchFunction(None)

    def onBtStartChoices(self, event):
        #change the choices
        self._ctrl.SetMultipleChoices( [ ("Zoey","WOW"), ("Alpha", "wxPython"),
                                    ("Ceda","Is"), ("Beta", "fantastic"),
                                    ("zoebob", "!!")], colFetch = 1 )
        self._ctrl.SetEntryCallback(None)
        self._ctrl.SetMatchFunction(None)

    def onBtDynamicChoices(self, event):
        '''
        Demonstrate dynamic adjustment of the auto-complete list, based on what's
        been typed so far:
        '''
        self._ctrl.SetChoices(self.dynamic_choices)
        self._ctrl.SetEntryCallback(self.setDynamicChoices)
        self._ctrl.SetMatchFunction(self.match)


    def match(self, text, choice):
        '''
        Demonstrate "smart" matching feature, by ignoring http:// and www. when doing
        matches.
        '''
        t = text.lower()
        c = choice.lower()
        if c.startswith(t): return True
        if c.startswith(r'http://'): c = c[7:]
        if c.startswith(t): return True
        if c.startswith('www.'): c = c[4:]
        return c.startswith(t)

    def setDynamicChoices(self):
        ctrl = self._ctrl
        text = ctrl.GetValue().lower()
        current_choices = ctrl.GetChoices()
        choices = [choice for choice in self.dynamic_choices if self.match(text, choice)]
        if choices != current_choices:
            ctrl.SetChoices(choices)

    def selectCallback(self, values):
        """ Simply function that receive the row values when the
            user select an item
        """
        print "Select Callback called...:",  values


if __name__ == "__main__":
    test()

